﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.Linq.Mapping;
using System.Linq;
using System.Reflection;
using CSW.Framework.Common.Binding.Interfaces;

namespace CSW.Framework.Common.Binding
{
    /// <summary>
    /// Linked Binding List
    /// </summary>
    /// <typeparam name="TDisplay">The type of the binding list.</typeparam>
    /// <typeparam name="TBusinessObject">The type of the other binding list.</typeparam>
    public class LinkedBindingList<TDisplay, TBusinessObject> : BindingList<TDisplay>, ILinkedBindingList<TBusinessObject>
        where TDisplay : IExtensionOf<TBusinessObject>
        where TBusinessObject : IBusinessObject, new()
    {
        private readonly BindingList<TBusinessObject> m_OtherBindingList;
        private readonly object m_OtherBindingListLocker = new object();
        private readonly ConstructorInfo m_ThisConstructor;
        private readonly List<Key> m_KeyList = new List<Key>();
        private const BindingFlags m_PublicInstanceBindingFlags = BindingFlags.Instance | BindingFlags.Public;

        private class Key
        {
            public MethodInfo GetThisKey { get; private set; }
            public MethodInfo GetOtherKey { get; private set; }

            public Key(MethodInfo getThisKey, MethodInfo getOtherKey)
            {
                Guard.ArgumentNotNull(getThisKey, "getThisKey");
                Guard.ArgumentNotNull(getOtherKey, "getOtherKey");

                GetThisKey = getThisKey;
                GetOtherKey = getOtherKey;
            }
        }

        /*/// <summary>
        /// Initializes a new instance of the <see cref="LinkedBindingList&lt;T, U&gt;"/> class.
        /// </summary>
        /// <param name="thisList">This list.</param>
        /// <param name="otherList">The other list.</param>
        public LinkedBindingList(IList<TDisplay> thisList, IList<TBusinessObject> otherList)
            : this(thisList, otherList, typeof(TBusinessObject).GetProperties(m_PublicInstanceBindingFlags).Where(p => p.GetCustomAttributes(true).OfType<ColumnAttribute>().Where(q => q.IsPrimaryKey).Any()).Select(p => p.Name).ToList().ToArray())
        {
        }*/

        /// <summary>
        /// Initializes a new instance of the <see cref="LinkedBindingList&lt;T, U&gt;"/> class.
        /// </summary>
        /// <param name="thisList">This list.</param>
        /// <param name="otherList">The other list.</param>
        /// <param name="primaryKeys">The primary keys.</param>
        public LinkedBindingList(IList<TDisplay> thisList, IList<TBusinessObject> otherList, string[] primaryKeys)
        {
            Guard.ArgumentNotNull(thisList, "thisList");
            Guard.ArgumentNotNull(otherList, "otherList");
            Guard.ArgumentNotNullOrEmpty<string>(primaryKeys, "primaryKeys");

            AllowNew = true;

            m_ThisConstructor = typeof(TDisplay).GetConstructor(new[] { typeof(TBusinessObject) });
            if (m_ThisConstructor == null)
                throw new InvalidOperationException(string.Format("Type '{0}' does not contains a constructor which takes '{1}' as its only argument.", typeof(TDisplay), typeof(TBusinessObject)));

            for (int i = 0; i < primaryKeys.Length; i++)
            {
                string primaryKey = primaryKeys[i];
                if (string.IsNullOrEmpty(primaryKey))
                {
                    string argumentName = string.Format("primaryKey[{0}]", i);
                    throw new ArgumentNullException(argumentName, string.Format("'{0}' cannot be null or empty string.", argumentName));
                }

                PropertyInfo thisKey = typeof(TDisplay).GetProperty(primaryKey, m_PublicInstanceBindingFlags);
                if (thisKey == null)
                    throw new InvalidOperationException(string.Format("Type '{0}' does not have a property '{1}'.", typeof(TDisplay), primaryKey));
                PropertyInfo otherKey = typeof(TBusinessObject).GetProperty(primaryKey, m_PublicInstanceBindingFlags);
                if (otherKey == null)
                    throw new InvalidOperationException(string.Format("Type '{0}' does not have a property '{1}'.", typeof(TBusinessObject), primaryKey));

                MethodInfo getThisKey = thisKey.GetGetMethod();
                if (getThisKey == null)
                    throw new InvalidOperationException(string.Format("Type '{0}' does not have a public get accessor on property '{1}'.", typeof(TDisplay), primaryKey));
                MethodInfo getOtherKey = otherKey.GetGetMethod();
                if (getOtherKey == null)
                    throw new InvalidOperationException(string.Format("Type '{0}' does not have a public get accessor on property '{1}'.", typeof(TBusinessObject), primaryKey));

                if (thisKey.PropertyType != otherKey.PropertyType)
                    throw new InvalidOperationException(string.Format("Type '{0}' property '{1}' has type '{2}'; Type '{3}' property '{1}' has type '{4}'; their types must match.", typeof(TDisplay), primaryKey, thisKey.PropertyType, typeof(TBusinessObject), otherKey.PropertyType));

                Key key = new Key(getThisKey, getOtherKey);
                m_KeyList.Add(key);
            }

            foreach (TDisplay item in thisList)
                Add(item);

            m_OtherBindingList = new BindingList<TBusinessObject>();
            foreach (TBusinessObject item in otherList)
                m_OtherBindingList.Add(item);

            m_OtherBindingList.ListChanged += m_OtherBindingList_ListChanged;

            if (m_OtherBindingList.Count != Count)
                throw new InvalidOperationException ("The lists are not equal in length.");

            for (int i = 0; i < Count; i++)
            {
                List<object> list1 = GetKey(this[i]);
                List<object> list2 = GetKey(m_OtherBindingList[i]);

                for (int j=0; j<list1.Count; j++)
                {
                    if (!list1[j].Equals(list2[j]))
                        throw new InvalidOperationException(string.Format("The lists differ at position {0}.", i));
                }
            }
        }

        /// <summary>
        /// Gets the other binding list.
        /// </summary>
        /// <returns>The other binding list.</returns>
        public BindingList<TBusinessObject> GetLinkedBindingList()
        {
            return m_OtherBindingList;
        }

        /// <summary>
        /// Adds a new item to the end of the collection.
        /// </summary>
        /// <returns>The item that was added to the collection.</returns>
        /// <exception cref="T:System.InvalidCastException">The new item is not the same type as the objects contained in the <see cref="T:System.ComponentModel.BindingList`1"/>.</exception>
        protected override object AddNewCore()
        {
            TBusinessObject newItem = new TBusinessObject();
            BindingList<TBusinessObject> bindingList = GetLinkedBindingList();
            bindingList.Add(newItem);
            return this[bindingList.IndexOf(newItem)];
        }

        private void m_OtherBindingList_ListChanged(object sender, ListChangedEventArgs e)
        {
            lock (m_OtherBindingListLocker)
            {
                switch (e.ListChangedType)
                {
                    case ListChangedType.Reset:
                        throw new NotSupportedException();

                    case ListChangedType.ItemAdded:
                        TBusinessObject addedItem = m_OtherBindingList[e.NewIndex];
                        TDisplay newItem = (TDisplay)m_ThisConstructor.Invoke(new object[] { addedItem });
                        Insert(e.NewIndex, newItem);
                        break;

                    case ListChangedType.ItemChanged:
                        // not interested?
                        break;

                    case ListChangedType.ItemDeleted:
                        //U deletedItem = m_OtherBindingList[];
                        //int thisIndex = FindThisIndexOf(deletedItem);
                        RemoveAt(e.NewIndex);
                        break;

                    case ListChangedType.ItemMoved:
                        TDisplay item1 = this[e.NewIndex];
                        TDisplay item2 = this[e.OldIndex];
                        SetItem(e.NewIndex, item2);
                        SetItem(e.OldIndex, item1);
                        break;
                }
            }
        }

        private List<object> GetKey(TBusinessObject item)
        {
            List<object> itemKeys = new List<object>();
            foreach (Key key in m_KeyList)
            {
                itemKeys.Add(key.GetOtherKey.Invoke(item, null));
            }

            return itemKeys;
        }

        private List<object> GetKey(TDisplay item)
        {
            List<object> itemKeys = new List<object>();
            foreach (Key key in m_KeyList)
            {
                itemKeys.Add(key.GetThisKey.Invoke(item, null));
            }

            return itemKeys;
        }

        private int FindThisIndexOf(TBusinessObject otherItem)
        {
            Guard.ArgumentNotNull(otherItem, "otherItem");

            List<object> itemKeys = GetKey(otherItem);

            int index = 0;
            foreach (TDisplay thisItem in this)
            {
                int i = 0;
                bool found = true;
                foreach (Key key in m_KeyList)
                {
                    if (key.GetThisKey.Invoke(thisItem, null) != itemKeys[i])
                    {
                        found = false;
                        break;
                    }
                    i++;
                }

                if (found)
                    return index;

                index++;
            }

            return -1;
        }
    }
}
